
package jpoco.client;

import java.net.URL;
import java.util.*;

import jpoco.io.Deserializer;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import net.oauth.OAuthServiceProvider;
import net.oauth.client.OAuthClient;
import net.oauth.client.httpclient4.HttpClient4;
import net.oauth.client.httpclient4.HttpClientPool;

import jpoco.client.Response;
import jpoco.client.Response.*;

/**
 * 
 * 
 *   Portable Contacts client  (aka Portable Contacts consumer)
 *   
 *   @see http://portablecontacts.net
 * 
 *   @author Sean C. Sullivan
 *   
 */
public class Client
{
	private HttpClient httpClient;
	private OAuthClient authClient;
	private OAuthServiceProvider authProvider;
	private Token requestToken = new Token();
	private ClientSettings settings;
	private boolean compressionEnabled = false;
	private HttpClientPool pool;

	static
	{
//		System.getProperties().put("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
//		System.getProperties().put("org.apache.commons.logging.simplelog.defaultlog", "trace");
	}
	
	public Client()
	{
		this(new DefaultHttpClient());
	}
	
	public Client(final HttpClient hClient)
	{
		this.httpClient = hClient;
		
		this.setUserAgent("jpoco");
		
		this.setSocketTimeout( (30 * 1000) );
		this.setConnectionTimeout( (5 * 1000) );
		
		
		this.pool = new HttpClientPool()
		{

			public HttpClient getHttpClient(URL u)
			{
				return httpClient;
			}
			
		};
		
	}

	public void setCompressionEnabled(boolean b)
	{
		compressionEnabled = b;
	}
	

	
	protected <T extends Response> T sendPortableContactsRequest(java.util.Map<String, String> params, Class<? extends Response> responseClass)
	{
		return sendPortableContactsRequest(params, null, responseClass);
	}

	protected <T extends Response> T sendPortableContactsRequest(String extraPath, Class<? extends Response> responseClass)
	{
		return sendPortableContactsRequest( null, extraPath, responseClass);
	}
	
	protected <T extends Response> T sendPortableContactsRequest(java.util.Map<String, String> params, String extraPath, Class<? extends Response> responseClass)
	{
		
		String rsp = sendHttpRequest(this.getClientSettings().getEndpointUrl(), 
					 						"GET", 
					 						extraPath,
					 						params, 
					 						this.getClientSettings().getUserSpecificAccessToken());
		
		return fromString(rsp, responseClass);
		
	}
	
	protected String sendHttpRequest(String baseUrl, String method, String extraPath, java.util.Map<String, String> params, Token token)
	{
		
		OAuthClient client = getOAuthClient();
		
		
		OAuthMessage responseMsg = null;
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		access.accessToken = token.getPublicToken();
		access.tokenSecret = token.getSecret();
		
		if (params == null)
		{
			params = new HashMap<String, String>();
		}
		
		 
		params.put("format", this.getClientSettings().getFormat().getType());
		
		params.put("fields", "@all");
		
		if (this.getClientSettings().getScope() != null)
		{
			params.put("scope", this.getClientSettings().getScope());
		}
		
		String url = baseUrl;
		
		if (extraPath != null)
		{
			url += extraPath;
		}
		
		try
		{
			
			responseMsg = client.invoke(access, method, url, params.entrySet());
			
			return responseMsg.readBodyAsString();
			
		} 
		catch (Exception e) 
		{
			throw new RuntimeException(e);
		}
		
	}

	protected <T extends Response> T fromString(String rsp, Class<? extends Response> responseClass)
	{
		System.out.println("Server response: " + rsp);
		
		Deserializer des = new Deserializer(this.getClientSettings().getFormat(), responseClass);
		
		Response r = des.deserialize(rsp, responseClass);
		
		r.setResponseString(rsp);
		
		return (T) r;
	}
	

	public Token getRequestToken()
	{
		return this.requestToken;
	}

	protected OAuthClient createOAuthClient()
	{
		
		OAuthClient authClient = new OAuthClient(new HttpClient4(this.pool));
		
		return authClient;
	}

	protected OAuthClient getOAuthClient()
	{
		
		if (authClient == null)
		{
			authClient = createOAuthClient();
		}
		
		return authClient;
	}

	protected OAuthServiceProvider createOAuthServiceProvider()
	{
		
		OAuthServiceProvider p = new OAuthServiceProvider(
				this.getClientSettings().getOAuthRequestTokenUrl(),
				this.getClientSettings().getOAuthAuthorizeUrl(),
				this.getClientSettings().getOAuthAccessTokenUrl());
		
		return p;
		
	}
	
	public ClientSettings getClientSettings()
	{
		return this.settings; 
	}

	protected OAuthServiceProvider getOAuthServiceProvider()
	{
		if (authProvider == null)
		{
			authProvider = createOAuthServiceProvider();
		}
		return authProvider;
	}
	
	protected OAuthAccessor createOAuthAccessor()
	{
		
		OAuthConsumer c = createOAuthConsumer();
		
		OAuthAccessor a = new OAuthAccessor(c);
		
		return a;
		
	}

	
	protected OAuthConsumer createOAuthConsumer()
	{
		
		Token consumerToken = this.getClientSettings().getConsumerToken();
		
		String oauthCallBackUrl = this.getClientSettings().getOAuthCallbackUrl();
		
		OAuthConsumer c = new OAuthConsumer(
				oauthCallBackUrl, 
				consumerToken.getPublicToken(), 
				consumerToken.getSecret(), 
				getOAuthServiceProvider());

		

		if (this.compressionEnabled)
		{
			c.setProperty(OAuthConsumer.ACCEPT_ENCODING, "gzip, deflate");
		}
		
		
		return c;
	}
	
	/**
	 * 
	 * @return all contacts
	 * 
	 */
	public List<Contact> getContacts()
	{
		
		MultipleContactResponse r = sendPortableContactsRequest("/@me/@all", MultipleContactResponse.class);
		
		List<Contact> contacts = r.getContacts();
		
		return contacts;
	}
	
	public List<Contact> getFriends()
	{
		MultipleContactResponse r = sendPortableContactsRequest("/@me/@friends", MultipleContactResponse.class);
		
		List<Contact> contacts = r.getContacts();
		
		return contacts;
	}
	
	/**
	 * 
	 * 
	 * @return may return null
	 * 
	 */
	public Contact getContact(String id)
	{
		SingleContactResponse r = sendPortableContactsRequest("/@me/@all/" + id, SingleContactResponse.class);
		
		return r.getContact();
		
	}
	
	/**
	 * 
	 * 
	 * @return may return null
	 * 
	 */
	public Contact getSelf()
	{
		SingleContactResponse r = sendPortableContactsRequest("/@me/@self", SingleContactResponse.class);
		
		Contact c = r.getContact();
		
		return c;
		
	}
	
	public List<Contact> query(QueryParameters queryParams)
	{
		
		Map<String, String> paramMap = Collections.emptyMap();
		
		if (queryParams != null)
		{
			paramMap = queryParams.getUrlParamMap(); 
		}
		
		MultipleContactResponse r = sendPortableContactsRequest(paramMap, "/@me/@all", MultipleContactResponse.class);
		
		return r.getContacts();
	}
	
	
	protected void checkUserAccessToken() throws UserAuthorizationRequiredException
	{
		
		if ( hasValidUserAccessToken() == false)
		{
			
			if ( (this.requestToken != null) && (this.requestToken.isValid()) )
			{
				fetchAccessToken();
			}
			else
			{
				String userAuthorizationUrl = getUserAuthorizationUrl();
				
				throw new UserAuthorizationRequiredException(userAuthorizationUrl);
			}
		}
	}

	public void fetchAccessToken()
	{

		OAuthClient client = getOAuthClient();
		
		OAuthMessage responseMsg = null;
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		
		System.out.println("Request token: " + requestToken);
		System.out.println("Consumer token: " + this.getClientSettings().getConsumerToken());
		
		try
		{
			
				//
				//  request the Access token
				//
				
				access.accessToken = requestToken.getPublicToken();
				access.requestToken = requestToken.getPublicToken();
				access.tokenSecret = requestToken.getSecret();
				
				responseMsg = client.invoke(access, "GET", this.getClientSettings().getOAuthAccessTokenUrl(), null);
				
				this.getUserSpecificAccessToken().setPublicToken(responseMsg.getParameter("oauth_token"));
				this.getUserSpecificAccessToken().setSecret(responseMsg.getParameter("oauth_token_secret"));
			
			
		} 
		catch (Exception e) 
		{
			throw new RuntimeException(e);
		}
	}
	
	public String getUserAuthorizationUrl()
	{
		
		String url = null;

		OAuthClient client = getOAuthClient();
		
		
		OAuthAccessor access = this.createOAuthAccessor();
		
		
		try
		{
			
			this.requestToken = null;
			access.requestToken = null;
			access.accessToken = null;
			access.tokenSecret = null;

			//
			//  send HTTP request to the Request Token URL
			//
			
			Map<String, String> parameters = new HashMap<String, String>();
			if (this.getClientSettings().getScope() != null)
			{
				parameters.put("scope", this.getClientSettings().getScope());
			}
			
			client.getRequestToken(access, "GET", parameters.entrySet());
			
			
			// store the Request Token values
			this.requestToken = new Token(access.requestToken, access.tokenSecret.trim());
			
			
			//
			//
			// build user authorization URL
			//
			//
			
			url = this.getClientSettings().getOAuthAuthorizeUrl() 
						+ "?oauth_token=" + requestToken.getPublicToken();
			
			if (settings.getOAuthCallbackUrl() != null)
			{
					url = url + "&oauth_callback="
								+ settings.getOAuthCallbackUrl();
			}
			
			
			return url;
			
		} 
		catch (Exception e) 
		{
			throw new RuntimeException(e);
		}
		
		
	}

	protected boolean hasValidUserAccessToken()
	{
		return this.getUserSpecificAccessToken().isValid();
	}

	protected Token getUserSpecificAccessToken()
	{
		return this.getClientSettings().getUserSpecificAccessToken(); 
	}

	public void setClientSettings(ClientSettings s)
	{
		this.settings = s;
	}

	public void setRequestToken(Token t)
	{
		this.requestToken = t;
	}
	
	public byte[] get(String url)
	{
		HttpGet get = null;
		HttpResponse response = null;
		byte[] data = null;
		HttpEntity entity = null;
		
		try
		{
			get = new HttpGet(url);
			response = this.getHttpClient().execute(get);
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
			{
				entity = response.getEntity();
				data = EntityUtils.toByteArray(entity);
				return data;
			}
			else
			{
				throw new RuntimeException("HTTP GET failed ("
								+ url
								+ "), status code = " 
								+ response.getStatusLine().getStatusCode());
			}
		}
		catch (RuntimeException ex)
		{
			throw ex;
		}
		catch (Exception ex)
		{
			throw new RuntimeException(ex);
		}
		
		
	}

	protected HttpClient getHttpClient()
	{
		return this.httpClient;
	}

	public void shutdown()
	{
		try
		{
			this.getHttpClient().getConnectionManager().shutdown();
		}
		catch (Exception ignore)
		{
			// ignored
		}
	}
	

	public void downloadAllPhotos(List<Contact> contacts)
	{
		for (Contact c : contacts)
		{
			downloadAllPhotos(c);
		}
	}
	
	
	public void downloadAllPhotos(Contact c)
	{
		downloadPhotos(c.getPhotos());
	}
	
	public void downloadPhotos(List<Photo> photos)
	{
		for (Photo p : photos)
		{
			downloadPhoto(p);
		}
	}
	
	public void downloadPhoto(Photo p)
	{
		if (p.getValue() != null)
		{
			byte[] data = this.get(p.getValue());
			p.setData(data);
		}
	}

	public void setUserAgent(String ua)
	{
		this.getHttpClient().getParams().setParameter(AllClientPNames.USER_AGENT, ua);
	}
	
	public void setConnectionTimeout(int milliseconds)
	{
		this.getHttpClient().getParams().setIntParameter(AllClientPNames.CONNECTION_TIMEOUT, milliseconds);
	}
	
	public void setSocketTimeout(int milliseconds)
	{
		this.getHttpClient().getParams().setIntParameter(AllClientPNames.SO_TIMEOUT, milliseconds);
	}
	

	public String toString()
	{
		if (this.getClientSettings() != null)
		{
			return this.getClientSettings().toString();
		}
		else
		{
			return super.toString();
		}
	}
}
